﻿// 文件使用MiniExcel源码， 使用时需遵循其 Apache License 2.0
// 修改摘要通知： 删除异步、Task等net2.0中不支持的内容，替换原先C#自带的zip处理方式，改用yacomp 进行解压缩。
using ExcelConfig.Attributes;
using ExcelConfig.helpers;
using ExcelConfig.OpenXml;
using ExcelConfig.OpenXml.Models;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
    
namespace ExcelConfig.Utils
{
    internal class ExcelColumnInfo
    {
        public object Key { get; set; }
        public int? ExcelColumnIndex { get; set; }
        public string ExcelColumnName { get; set; }
        public string[] ExcelColumnAliases { get; set; }
        public Property Property { get; set; }
        public Type ExcludeNullableType { get; set; }
        public bool Nullable { get; internal set; }
        public string ExcelFormat { get; internal set; }
        public double? ExcelColumnWidth { get; internal set; }
        public string ExcelIndexName { get; internal set; }
        public bool ExcelIgnore { get; internal set; }
        public int ExcelFormatId { get; internal set; }
        public ColumnType ExcelColumnType { get; internal set; }
        public DynamicExcelColumn.Formatt CustomFormatter { get; set; }
    }

    internal class ExcellSheetInfo
    {
        public object Key { get; set; }
        public string ExcelSheetName { get; set; }
        public SheetState ExcelSheetState { get; set; }
        
        private string ExcelSheetStateAsString => ExcelSheetState.ToString().ToLower();

        public SheetDto ToDto(int sheetIndex)
        {
            return new SheetDto { Name = ExcelSheetName, SheetIdx = sheetIndex, State = ExcelSheetStateAsString };
        }
    }

    internal static class CustomPropertyHelper
    {
        internal static IDictionary<string, object> GetEmptyExpandoObject(int maxColumnIndex, int startCellIndex)
        {
            var cell = new Dictionary<string, object>();
            for (int i = startCellIndex; i <= maxColumnIndex; i++)
            {
                var key = ColumnHelper.GetAlphabetColumnName(i);
                if (!cell.ContainsKey(key))
                    cell.Add(key, null);
            }
            return cell;
        }

        internal static IDictionary<string, object> GetEmptyExpandoObject(Dictionary<int, string> hearrows)
        {
            var cell = new Dictionary<string, object>();
            foreach (var hr in hearrows)
                if (!cell.ContainsKey(hr.Value))
                    cell.Add(hr.Value, null);
            return cell;
        }

        internal static List<ExcelColumnInfo> GetSaveAsProperties(Type type, Configuration configuration)
        {
            var props = new List<ExcelColumnInfo>();
            foreach (var info in GetExcelPropertyInfo(type, BindingFlags.Public | BindingFlags.Instance, configuration))
                if (info.Property.CanRead) props.Add(info);

            if (props.Count == 0)
                throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0");

            return SortCustomProps(props);
        }

        internal static List<ExcelColumnInfo> SortCustomProps(List<ExcelColumnInfo> props)
        {
            // https://github.com/mini-software/MiniExcel/issues/142
            //TODO: need optimize performance

            var withCustomIndexProps = new List<ExcelColumnInfo>();
            foreach (var prop in props) if (prop.ExcelColumnIndex != null && prop.ExcelColumnIndex > -1) withCustomIndexProps.Add(prop);

            {
                var a = new List<int?>();
                foreach (var prop in withCustomIndexProps)
                {
                    if (a.Contains(prop.ExcelColumnIndex)) throw new InvalidOperationException("Duplicate column name");
                    a.Add(prop.ExcelColumnIndex);
                }
            }

            var maxColumnIndex = props.Count - 1;
            if (withCustomIndexProps.Count != 0)
                maxColumnIndex = Math.Max((int)helper.Max(withCustomIndexProps, w => w.ExcelColumnIndex.HasValue?w.ExcelColumnIndex.Value:0), maxColumnIndex);

            var withoutCustomIndexProps = new List<ExcelColumnInfo>();
            foreach (var prop in props) if (prop.ExcelColumnIndex == null || prop.ExcelColumnIndex == -1) withoutCustomIndexProps.Add(prop); ;

            var newProps = new List<ExcelColumnInfo>();
            var index = 0;
            for (int i = 0; i <= maxColumnIndex; i++)
            {
                var p1 = withCustomIndexProps.Find(s => s.ExcelColumnIndex == i);
                if (p1 != null)
                {
                    newProps.Add(p1);
                }
                else
                {
                    if (withoutCustomIndexProps.Count > index)
                    {
                        withoutCustomIndexProps[index].ExcelColumnIndex = i;
                        newProps.Add(withoutCustomIndexProps[index]);
                    }
                    else
                    {
                        newProps.Add(null);
                    }
                    index++;
                }
            }
            return newProps;
        }

        internal static List<ExcelColumnInfo> GetExcelCustomPropertyInfos(Type type, List<string> keys, Configuration configuration)
        {
            var flags = BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance;
            var props = new List<ExcelColumnInfo>();
            foreach (var prop in GetExcelPropertyInfo(type, flags, configuration))
                if (prop.Property.Info.GetSetMethod() != null // why not .Property.CanWrite? because it will use private setter
                               && !AttributeExtension.GetAttributeValue<ExcelIgnoreAttribute, bool>(prop.Property.Info, (ExcelIgnoreAttribute x) => x.ExcelIgnore)
                               && !AttributeExtension.GetAttributeValue<ExcelColumnAttribute, bool>(prop.Property.Info, (ExcelColumnAttribute x) => x.Ignore))
                    props.Add(prop);

            if (props.Count == 0)
                throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0");

            var withCustomIndexProps = new List<ExcelColumnInfo>();
            foreach (var prop in props) if (prop.ExcelColumnIndex != null && prop.ExcelColumnIndex > -1)withCustomIndexProps.Add(prop);

            {
                var a = new List<int?>();
                foreach (var prop in withCustomIndexProps)
                {
                    if (a.Contains(prop.ExcelColumnIndex)) throw new InvalidOperationException("Duplicate column name");
                    a.Add(prop.ExcelColumnIndex);
                }
            }

            var maxkey = keys[keys.Count-1];
            var maxIndex = ColumnHelper.GetColumnIndex(maxkey);
            foreach (var p in props)
            {
                if (p.ExcelColumnIndex == null)
                    continue;
                if (p.ExcelColumnIndex > maxIndex)
                    throw new ArgumentException($"ExcelColumnIndex {p.ExcelColumnIndex} over haeder max index {maxkey}");
                if (p.ExcelColumnName == null)
                    throw new InvalidOperationException($"{p.Property.Info.DeclaringType?.Name} {p.Property.Name}'s ExcelColumnIndex {p.ExcelColumnIndex} can't find excel column name");
            }

            return props;
        }

        internal static string DescriptionAttr(Type type, object source)
        {
            var name = source?.ToString();
            return AttributeExtension.GetAttribute<DescriptionAttribute>(type
                .GetField(name))?
                .Description
                ?? name;
        }

        private static IEnumerable<ExcelColumnInfo> ConvertToExcelCustomPropertyInfo(PropertyInfo[] props, Configuration configuration)
        {
            // solve : https://github.com/mini-software/MiniExcel/issues/138
            foreach (var p in props)
            {

                var gt = Nullable.GetUnderlyingType(p.PropertyType);
                var excelColumnName = AttributeExtension.GetAttribute<ExcelColumnNameAttribute>(p);
                var excludeNullableType = gt ?? p.PropertyType;
                var excelFormat = AttributeExtension.GetAttribute<ExcelFormatAttribute>(p)?.Format;
                var excelColumn = AttributeExtension.GetAttribute<ExcelColumnAttribute>(p);
                var dynamicColumn = helper.SingleOrDefault(configuration?.DynamicColumns, _ => _.Key == p.Name);
                if (dynamicColumn != null)
                    excelColumn = dynamicColumn;

                var ignore = AttributeExtension.GetAttributeValue<ExcelIgnoreAttribute, bool>(p, (ExcelIgnoreAttribute x) => x.ExcelIgnore) ||
                             AttributeExtension.GetAttributeValue<ExcelColumnAttribute, bool>(p, (ExcelColumnAttribute x) => x.Ignore) ||
                             (excelColumn?.Ignore ?? false);
                if (ignore) continue;

                //TODO:or configulation Dynamic
                var excelColumnIndex = excelColumn?.Index > -1 ? excelColumn.Index : (int?)null;
                yield return new ExcelColumnInfo
                {
                    Property = new Property(p),
                    ExcludeNullableType = excludeNullableType,
                    Nullable = gt != null,
                    ExcelColumnAliases = excelColumnName?.Aliases ?? excelColumn?.Aliases,
                    ExcelColumnName = excelColumnName?.ExcelColumnName ?? AttributeExtension.GetAttribute<DisplayNameAttribute>(p)?.DisplayName ?? excelColumn?.Name ?? p.Name,
                    ExcelColumnIndex = AttributeExtension.GetAttribute<ExcelColumnIndexAttribute>(p)?.ExcelColumnIndex ?? excelColumnIndex,
                    ExcelIndexName = AttributeExtension.GetAttribute<ExcelColumnIndexAttribute>(p)?.ExcelXName ?? excelColumn?.IndexName,
                    ExcelColumnWidth = AttributeExtension.GetAttribute<ExcelColumnWidthAttribute>(p)?.ExcelColumnWidth ?? excelColumn?.Width,
                    ExcelFormat = excelFormat ?? excelColumn?.Format,
                    ExcelFormatId = excelColumn?.FormatId ?? -1,
                    ExcelColumnType = excelColumn?.Type ?? ColumnType.Value,
                    CustomFormatter = dynamicColumn?.CustomFormatter
                };
            }
        }

        private static IEnumerable<ExcelColumnInfo> GetExcelPropertyInfo(Type type, BindingFlags bindingFlags, Configuration configuration)
        {
            //TODO:assign column index
            return ConvertToExcelCustomPropertyInfo(type.GetProperties(bindingFlags), configuration);
        }

        internal static ExcellSheetInfo GetExcellSheetInfo(Type type, Configuration configuration)
        {
            // default options
            var sheetInfo = new ExcellSheetInfo
            {
                Key = type.Name,
                ExcelSheetName = null, // will be generated automatically as Sheet<Index>
                ExcelSheetState = SheetState.Visible
            };

            // options from ExcelSheetAttribute
            if (AttributeExtension.GetAttribute<ExcelSheetAttribute>(type) is ExcelSheetAttribute excelSheetAttr)
            {
                sheetInfo.ExcelSheetName = excelSheetAttr.Name ?? type.Name;
                sheetInfo.ExcelSheetState = excelSheetAttr.State;
            }

            // options from DynamicSheets configuration
            var openXmlCOnfiguration = configuration as OpenXmlConfiguration;
            if (openXmlCOnfiguration?.DynamicSheets?.Length > 0)
            {
                var dynamicSheet = helper.SingleOrDefault(openXmlCOnfiguration.DynamicSheets, x => x.Key == type.Name);
                if (dynamicSheet != null)
                {
                    sheetInfo.ExcelSheetName = dynamicSheet.Name;
                    sheetInfo.ExcelSheetState = dynamicSheet.State;
                }
            }

            return sheetInfo;
        }

        internal static List<ExcelColumnInfo> GetDictionaryColumnInfo(IDictionary<string, object> dicString, IDictionary dic, Configuration configuration)
        {
            var props = new List<ExcelColumnInfo>();
            var keys = helper.to_list(dicString?.Keys.GetEnumerator()) ?? dic?.Keys
                       ?? throw new NotSupportedException();

            foreach (var key in keys) SetDictionaryColumnInfo(props, key, configuration);
            return SortCustomProps(props);
        }

        internal static void SetDictionaryColumnInfo(List<ExcelColumnInfo> props, object key, Configuration configuration)
        {
            var p = new ExcelColumnInfo
            {
                Key = key,
                ExcelColumnName = key?.ToString()
            };
            
            // TODO:Dictionary value type is not fiexed
            //var _t =
            //var gt = Nullable.GetUnderlyingType(p.PropertyType);
            var isIgnore = false;
            if (configuration.DynamicColumns != null && configuration.DynamicColumns.Length > 0)
            {
                var dynamicColumn = helper.SingleOrDefault<DynamicExcelColumn>(configuration.DynamicColumns.GetEnumerator(), x => x.Key == key?.ToString());
                if (dynamicColumn != null)
                {
                    p.Nullable = true;
                    //p.ExcludeNullableType = item2[key]?.GetType();
                    if (dynamicColumn.Format != null)
                    {
                        p.ExcelFormat = dynamicColumn.Format;
                        p.ExcelFormatId = dynamicColumn.FormatId;
                    }
                    if (dynamicColumn.Aliases != null)
                        p.ExcelColumnAliases = dynamicColumn.Aliases;
                    if (dynamicColumn.IndexName != null)
                        p.ExcelIndexName = dynamicColumn.IndexName;
                    p.ExcelColumnIndex = dynamicColumn.Index;
                    if (dynamicColumn.Name != null)
                        p.ExcelColumnName = dynamicColumn.Name;
                    isIgnore = dynamicColumn.Ignore;
                    p.ExcelColumnWidth = dynamicColumn.Width;
                    p.ExcelColumnType = dynamicColumn.Type;
                    p.CustomFormatter = dynamicColumn.CustomFormatter;
                }
            }
            if (!isIgnore)
                props.Add(p);
        }

        internal static bool TryGetTypeColumnInfo(Type type, Configuration configuration, out List<ExcelColumnInfo> props)
        {
            // Unknown type
            if (type == null)
            {
                props = null;
                return false;
            }

            if (type.IsValueType || type == typeof(string))
                throw new NotSupportedException($"MiniExcel does not support the use of {type.FullName} as a generic type");

            if (ValueIsNeededToDetermineProperties(type))
            {
                props = null;
                return false;
            }

            props = GetSaveAsProperties(type, configuration);
            return true;
        }
        internal static List<ExcelColumnInfo> GetColumnInfoFromValue(object value, Configuration configuration)
        {
            switch (value)
            {
                case IDictionary<string, object> genericDictionary:
                    return GetDictionaryColumnInfo(genericDictionary, null, configuration);
                case IDictionary dictionary:
                    return GetDictionaryColumnInfo(null, dictionary, configuration);
                default:
                    return GetSaveAsProperties(value.GetType(), configuration);
            }
        }

        private static bool ValueIsNeededToDetermineProperties(Type type) => type == typeof(object)
                    || typeof(IDictionary<string, object>).IsAssignableFrom(type)
                    || typeof(IDictionary).IsAssignableFrom(type);

        internal static ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName, Configuration configuration)
        {
            var prop = new ExcelColumnInfo
            {
                ExcelColumnName = columnName,
                Key = columnName
            };

            if (configuration.DynamicColumns == null || configuration.DynamicColumns.Length <= 0)
                return prop;

            var dynamicColumn = helper.SingleOrDefault(configuration.DynamicColumns, col => string.Equals(col.Key, columnName, StringComparison.OrdinalIgnoreCase));
            
            if (dynamicColumn == null || dynamicColumn.Ignore)
                return prop;

            prop.Nullable = true;
            prop.ExcelIgnore = dynamicColumn.Ignore;
            prop.ExcelColumnType = dynamicColumn.Type;
            prop.ExcelColumnIndex = dynamicColumn.Index;
            prop.ExcelColumnWidth = dynamicColumn.Width;
            prop.CustomFormatter = dynamicColumn.CustomFormatter;

            if (dynamicColumn.Format != null)
            {
                prop.ExcelFormat = dynamicColumn.Format;
                prop.ExcelFormatId = dynamicColumn.FormatId;
            }

            if (dynamicColumn.Aliases != null)
            {
                prop.ExcelColumnAliases = dynamicColumn.Aliases;
            }

            if (dynamicColumn.IndexName != null)
            {
                prop.ExcelIndexName = dynamicColumn.IndexName;
            }

            if (dynamicColumn.Name != null)
            {
                prop.ExcelColumnName = dynamicColumn.Name;
            }

            return prop;
        }
        
        internal static Dictionary<string, int> GetHeaders(IDictionary<string, object> item, bool trimNames = false)
        {
            var dict = new Dictionary<string, int>();
            var idx_map = new Dictionary<string, int>();
            var idx_map2 = new Dictionary<string, int>();
            var ix = 0;
            foreach (var dic in DictToNameWithIndex(item))
            {
                if (!idx_map.ContainsKey(dic.Name)) {
                    idx_map.Add(dic.Name, ix++);
                    idx_map2.Add(dic.Name, 0);
                }
                var idx = idx_map2[dic.Name];
                var name = idx == 0 ? dic.Name : $"{dic.Name}_____{idx + 1}";
                dict[trimNames ? name.Trim() : name] = idx_map[dic.Name];
                idx_map2[dic.Name]++;
            }
            return dict;
        }

        private static IEnumerable<NameIndexPair> DictToNameWithIndex(IDictionary<string, object> dict)
        {
            int i = 0;
            foreach (var obj in dict.Values) yield return new NameIndexPair(i++, obj?.ToString() ?? "");
        }
        private class NameIndexPair
        {
            public int Index { get; }
            public string Name { get; }

            public NameIndexPair(int index, string name)
            {
                Index = index;
                Name = name;
            }
        }
    }
}